package hisp;


import java.io.IOException;
import java.security.*;
import java.sql.SQLException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class Service extends Thread  {
	private SSLConnection sslDS;
	private SSLConnection sslKS;
	private SSLConnection sslClient;
	private Identity identity = null;
	private boolean isWork = true; 
	private DataBase database;
	private boolean isAuthorized = false;
	private AuditLog log = new AuditLog();

	public Service(SSLConnection ssl, String DsIp, int DsPort, String KsIp, int KsPort) {
		sslDS = new SSLConnection(DsIp, DsPort);
		sslKS = new SSLConnection(KsIp, KsPort);
		sslClient = ssl;
		this.database = new DataBase("seokje.iptime.org:3306", "hisp_phr database", "hie", "hie");
	}

	public void run() {
		Message message = null;
		System.out.println("thread created");
		try {

			while (isWork) {
				message = new Message(sslClient.readLine());
				System.out.println("message = " + message.toXml());
				if (message.getMessage().equals("newAccount")) {
					newAccount(message);
				}
				else if (message.getMessage().equals("logIn")) {
					logIn(message);					
				}
				else { 
					if (isAuthorized) {
						sslClient.writeLine(new Message().setMessage("accepted").toXml());
						if (message.getMessage().equals("createHealthRecord")){
							createHealthRecord(message);		
						}
						else if (message.getMessage().equals("requestHealthRecord")) {
							loadHealthRecord(message);
						}
						else if (message.getMessage().equals("alterHealthRecord")) {
							alterHealthRecord(message);

						}
						else if (message.getMessage().equals("addAccessControlList")) {
							addAccessControlList(message);
						}
						else if (message.getMessage().equals("revokeAccessControlList")){
							revokeAccessControlList(message);
						}
						else if (message.getMessage().equals("requestRole")) {
							sslClient.writeLine(new Message().setMessage(identity.role).toXml());
						}
						else if (message.getMessage().equals("logoff")) {
							break;
						}
						else {
							sslClient.writeLine(new Message().setMessage("denied").toXml());
						}	

					}
					else sslClient.writeLine(new Message().setMessage("denied").toXml());
				}
			}		
		} catch (Exception e) {			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		try {
			sslClient.close();
		} catch (Exception e1) {
			e1.printStackTrace();
		}

	}


	private void revokeAccessControlList(Message message) {
		String patientId = message.getHealthRecordAttribute("id");
		String identityXml = message.getIdentityAttribute("xml");
		try {
			sslDS.connect();
			sslDS.writeLine(new Message().setMessage("requestAccessControlList").setHealthRecord(patientId, null, null, null, null, null).toXml());

			message = new Message(sslDS.readLine());
			System.out.println("revoke = " + message.toXml());
			if (message.getMessage().equals("found")) {
				AccessControlList acl = new AccessControlList(message.getHealthRecordAttribute("acl"));
				String access = acl.hasAccess(new Identity(identityXml));
				System.out.println("revoke = " + access);

				if (access.equals("write") || access.equals("own")) {
					sslClient.writeLine(new Message().setMessage("authorized").setHealthRecord(null, null, null, acl.getXMLString(), null, null).toXml());

					message = new Message(sslClient.readLine());
					System.out.println("revoke - client = " + message.toXml());
					message.setMessage("alterAccessControlList")
					.setHealthRecord(patientId, null, null, null, null, null);

					System.out.println("revoke -data base = " + message.toXml());
					sslDS.connect();
					sslDS.writeLine(message.toXml());
					message = new Message(sslDS.readLine());
					System.out.println(message.toXml());
					sslClient.writeLine(message.toXml());

					log.writeLog(identity, "revoked AccessControlList");
				}
				else {
					sslClient.writeLine(new Message().setMessage("denied").toXml());
				}
			}
			else {
				sslClient.writeLine(new Message().setMessage("failed").toXml());						
			}
			sslDS.close();
		}
		catch (Exception e) {
			;
		}

	}

	private void addAccessControlList(Message message) {
		String patientId = message.getHealthRecordAttribute("id");
		String identityXml = message.getIdentityAttribute("xml");
		try {
			sslDS.connect();
			sslDS.writeLine(new Message().setMessage("requestAccessControlList").setHealthRecord(patientId, null, null, null, null, null).toXml());

			message = new Message(sslDS.readLine());
			System.out.println(message.toXml());
			if (message.getMessage().equals("found")) {
				AccessControlList acl = new AccessControlList(message.getHealthRecordAttribute("acl"));
				String access = acl.hasAccess(new Identity(identityXml));
				System.out.println("access = " + access);

				if (access.equals("write") || access.equals("own")) {
					sslClient.writeLine(new Message().setMessage("authorized").toXml());
					System.out.println("authorized");
					while (true) {
						message = new Message(sslClient.readLine());
						String newId = message.getMessage();
						if (newId.equals("end")) {
							sslClient.writeLine(message.toXml());
							return;
						}
						else {
							Identity newIdentity = findProfessional(message.getMessage());
							if (newIdentity == null) {
								sslClient.writeLine(new Message().setMessage("failed").toXml());
							}
							else {
								if (newIdentity.role.equals("doctor") || newIdentity.role.equals("nurse")) {
									String right;
									if (newIdentity.role.equals("doctor")) right = "write";
									else right = "read";

									acl.authorizeUser(newId, right);

									sslDS.connect();
									message = new Message().setMessage("alterAccessControlList").setHealthRecord(patientId, null, null, acl.getXMLString(), null, null);
									sslDS.writeLine(message.toXml());
									sslClient.writeLine(sslDS.readLine());
									sslDS.close();

									log.writeLog(identity, "added AccessControlList");
								}
								else {
									sslClient.writeLine(new Message().setMessage("failed").toXml());
								}
							}

						}
					}			
				}
				else {
					sslClient.writeLine(new Message().setMessage("denied").toXml());
				}	
			}
			else {
				sslClient.writeLine(new Message().setMessage("failed").toXml());
			}
		} catch (Exception e) { 
			e.printStackTrace();
		}
	}

	private void alterHealthRecord(Message message) {
		String patientId = message.getHealthRecordAttribute("id");
		String identityXml = message.getIdentityAttribute("xml");
		try {
			sslDS.connect();
			sslDS.writeLine(new Message().setMessage("requestAccessControlList").setHealthRecord(patientId, null, null, null, null, null).toXml());
			message = new Message(sslDS.readLine());

			if (message.getMessage().equals("found")) {

				AccessControlList acl = new AccessControlList(message.getHealthRecordAttribute("acl"));
				String access = acl.hasAccess(new Identity(identityXml));

				if (access.equals("write") || access.equals("own")) {
					sslClient.writeLine(new Message().setMessage("authorized").toXml());

					message = new Message(sslClient.readLine());

					String key[] = getKey(patientId); //TODO modified the function

					System.out.println(message.getHealthRecordAttribute("private"));
					System.out.println(message.getHealthRecordAttribute("public"));

					String encryptedPrivateRecord = encrypt(message.getHealthRecordAttribute("private"), key[0]);
					String encryptedPublicRecord = encrypt(message.getHealthRecordAttribute("public"), key[1]);

					System.out.println("enc priv = " + encryptedPrivateRecord);
					System.out.println("enc publ = " + encryptedPublicRecord);

					sslDS.connect();
					sslDS.writeLine(new Message().setMessage("alterHealthRecord").setHealthRecord(patientId, encryptedPrivateRecord, encryptedPublicRecord, null, null, null).toXml());

					message = new Message(sslDS.readLine());
					System.out.println(message.toXml());

					sslClient.writeLine(message.toXml());

					log.writeLog(identity, "altered Health Record");
				}
				else {
					sslClient.writeLine(new Message().setMessage("denied").toXml());
				}
			}
			else {
				sslClient.writeLine(new Message().setMessage("failed").toXml());
			}
			sslDS.close();
		}
		catch (Exception e) { 
			e.printStackTrace();
		}
	}

	private void loadHealthRecord(Message message) {
		String patientId = message.getHealthRecordAttribute("id");
		String identityXml = message.getIdentityAttribute("xml");
		try {
			sslDS.connect();
			sslDS.writeLine(new Message().setMessage("requestHealthRecord").setHealthRecord(patientId, null, null, null, null, null).toXml());

			message = new Message(sslDS.readLine());
			System.out.println("received HR from server \n" + message.toXml());
			String privateRecord, publicRecord;
			if (message.getMessage().equals("found")) {
				AccessControlList acl = new AccessControlList(message.getHealthRecordAttribute("acl"));
				String access = acl.hasAccess(new Identity(identityXml));

				if (!access.equals("denied")) {
					String key[] = getKey(patientId);
					System.out.println("key[0] = " + key[0]);
					System.out.println("key[1] = " + key[1]);
					System.out.println(message.toXml());
					privateRecord = decrypt(message.getHealthRecordAttribute("private"), key[0]);
					publicRecord = decrypt(message.getHealthRecordAttribute("public"), key[1]);
					System.out.println("public " + publicRecord);
					sslClient.writeLine(message.setMessage("authorized").setHealthRecord(null, privateRecord, publicRecord, null, null, null).toXml());

					log.writeLog(identity, "loaded Health Record");
				}
				else {
					sslClient.writeLine(new Message().setMessage("denied").toXml());
				}
			}
			else {
				sslClient.writeLine(new Message().setMessage("failed").toXml());
			}
			sslDS.close();
		}
		catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void createHealthRecord(Message message) {
		try {
			if (!identity.role.equals("doctor")) {
				System.out.println(identity.role + "is not doctor");
				sslClient.writeLine(new Message().setMessage("denied").toXml());
			}
			else {
				sslClient.writeLine(new Message().setMessage("authorized").toXml());

				while (true) {
					message = new Message(sslClient.readLine());
					if (newPatientAccount(message) == true) break;
				}

				message = new Message(sslClient.readLine());
				String patientId = message.getHealthRecordAttribute("id");
				String privateRecord = message.getHealthRecordAttribute("private");
				String publicRecord = message.getHealthRecordAttribute("public");

				KeyGenerator kgen = KeyGenerator.getInstance("AES");
				kgen.init(128);

				SecretKey skey = kgen.generateKey();
				byte[] raw = skey.getEncoded();

				String key[] = new String[2];
				key[0] = bytes2String(raw);
				skey = kgen.generateKey();
				raw = skey.getEncoded();
				key[1] = bytes2String(raw);


				String encryptedPrivateRecord = encrypt(privateRecord, key[0]);
				String encryptedPublicRecord = encrypt(publicRecord, key[1]);

				AccessControlList acl = new AccessControlList();
				acl.authorizeUser(identity.id, "own");
				acl.authorizeUser(patientId, "read");
				acl.authorizeRule("doctor", "emergency", "any", "write");	

				System.out.println("createHealthRecord ds send 1");				
				sslDS.connect();
				sslDS.writeLine(new Message().setMessage("createHealthRecord")
						.setIdentity(identity.id, null, null, null)
						.setHealthRecord(patientId, encryptedPrivateRecord, encryptedPublicRecord, acl.getXMLString(), null, null)
						.toXml());

				message = new Message(sslDS.readLine());
				System.out.println("createHealthRecord ds send 1 " + message.toXml());
				sslDS.close();
				if (message.getMessage().equals("created")) storeKey(patientId, key, message.getHealthRecordAttribute("identifier"));

				System.out.println("createHealthRecord " + message.toXml());
				sslClient.writeLine(message.toXml());


				log.writeLog(identity, "created Health Record");
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();

		}
	}

	private void logIn(Message message) throws SQLException, IOException {
		String id = message.getIdentityAttribute("id");
		String password = message.getIdentityAttribute("password");
		System.out.println("login pahse 1");
		try {
			database.connect();
			database.statement = database.connection.createStatement();
			database.resultSet = database.statement.executeQuery("SELECT * FROM accounts WHERE id='" + id + "'");
			database.resultSet.next();
			String storedPassword = database.resultSet.getString("password");
			String salt = database.resultSet.getString("salt");
			String xml = database.resultSet.getString("xml");

			System.out.println("login phase2");
			byte[] bSalt = string2Bytes(salt);

			String hashedPassword = bytes2String(getHash(password, bSalt));

			System.out.println("login pahse3");
			if (hashedPassword.equals(storedPassword)) {
				System.out.println("granted");
				sslClient.writeLine(new Message().setMessage("authorized").setIdentity(null, null, null, xml).toXml());
				identity = new Identity(xml);
			}
			else {
				System.out.println("denied");
				sslClient.writeLine(new Message().setMessage("denied").toXml());
				isWork = false;
			}			
		} catch (SQLException e) {
			sslClient.writeLine(new Message().setMessage("failed").toXml());
			isWork = false;
		} catch (IOException e) {
			isWork = false;
		} finally {
			database.statement.close();
			database.connection.close();
			isAuthorized = true;
		}
		System.out.println("login phase 4");
	}

	private void newAccount(Message message) throws SQLException {
		String id = message.getIdentityAttribute("id");
		String password = message.getIdentityAttribute("password");
		String salt = message.getIdentityAttribute("salt");
		String xml = message.getIdentityAttribute("xml");
		System.out.println(xml);
		try {
			database.connect();
			database.connection.setAutoCommit(false);
			database.preparedStatement = database.connection.prepareStatement("insert into accounts(id, password, salt, xml) values (?, ?, ?, ?)");
			database.preparedStatement.setString(1, id);
			database.preparedStatement.setString(2, password);
			database.preparedStatement.setString(3, salt);
			database.preparedStatement.setString(4, xml);
			database.preparedStatement.executeUpdate();
			database.connection.commit();	
			System.out.println("newAccount for " + id + " created");
			sslClient.writeLine(new Message().setMessage("created").toXml());

			identity = new Identity(xml);
			isAuthorized = true;

			log.writeLog(identity, "created new account");

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			try {
				sslClient.writeLine(new Message().setMessage("failed").toXml());
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
			isWork = false;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			database.preparedStatement.close();
			database.connection.close();
		}
	}

	private boolean newPatientAccount(Message message) throws SQLException {
		String id = message.getIdentityAttribute("id");
		String password = message.getIdentityAttribute("password");
		String salt = message.getIdentityAttribute("salt");
		String xml = message.getIdentityAttribute("xml");

		boolean done = true;;
		try {
			database.connect();
			database.connection.setAutoCommit(false);
			database.preparedStatement = database.connection.prepareStatement("insert into accounts(id, password, salt, xml) values (?, ?, ?, ?)");
			database.preparedStatement.setString(1, id);
			database.preparedStatement.setString(2, password);
			database.preparedStatement.setString(3, salt);
			database.preparedStatement.setString(4, xml);
			database.preparedStatement.executeUpdate();
			database.connection.commit();	
			System.out.println("newAccount for " + id + " created");
			sslClient.writeLine(new Message().setMessage("created").toXml());

			log.writeLog(identity, "created new account");
			done = true;

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			try {
				sslClient.writeLine(new Message().setMessage("failed").toXml());
				done = false;
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			database.preparedStatement.close();
			database.connection.close();
		}
		return done;
	}

	private boolean storeKey(String id, String key[], String identifier ) {
		try {			
			sslKS.connect();
			sslKS.writeLine(new Message().setMessage("updateKey").setKey(id, identifier, key[0], key[1]).toXml());
			Message message = new Message(sslKS.readLine());
			sslKS.close();
			if (message.getMessage().equals("create")) return true;
			else return false;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		}
	}

	private String[] getKey (String id) {
		try {
			sslKS.connect();
			sslKS.writeLine(new Message().setMessage("requestKey")
					.setKey(id, null, null, null).toXml());
			String temp = sslKS.readLine();
			System.out.println("ks = read line " + temp);
			Message message = new Message(temp);

			String key[] = new String[2];
			System.out.println("keystore server sent " + message.toXml());
			key[0] = message.getKeyAttribute("private");
			key[1] = message.getKeyAttribute("public");
			sslKS.close();
			return key;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			try {sslKS.close();}
			catch (Exception e1) {;}
			return null;
		}		
	}

	private Identity findProfessional(String id) {
		try {
			database.connect();
			database.connection.setAutoCommit(false);
			database.preparedStatement = database.connection.prepareStatement("select * from accounts where id = ?");
			database.preparedStatement.setString(1, id);
			database.resultSet = database.preparedStatement.executeQuery();
			database.resultSet.next();
			String i =database.resultSet.getString("xml");
			database.close();
			return new Identity(i);
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			return null;
		}		
	}


	private byte[] getHash(String string, byte[] salt) {	
		MessageDigest mda;
		try {
			mda = MessageDigest.getInstance("SHA-512");
			mda.reset();
			mda.update(salt);
			byte[] digest = mda.digest(string.getBytes());
			for (int i = 0; i < 1000; i++) {
				mda.reset();
				digest = mda.digest(digest);
			}

			return digest;

		} catch (NoSuchAlgorithmException e) {
			return null;
		} 
	}

	/*
	 * http://stackoverflow.com/questions/521101/using-sha1-and-rsa-with-java-security-signature-vs-messagedigest-and-cipher
	 */
	private String bytes2String(byte[] bytes) {
		StringBuilder string = new StringBuilder();
		for (byte b: bytes) {
			String hexString = Integer.toHexString(0x00FF & b);
			string.append(hexString.length() == 1 ? "0" + hexString : hexString);
		}
		return string.toString();
	}

	/*
	 * http://stackoverflow.com/questions/140131/convert-a-string-representation-of-a-hex-dump-to-a-byte-array-using-java
	 */
	public byte[] string2Bytes(String s) {
		int len = s.length();
		byte[] data = new byte[len / 2];
		for (int i = 0; i < len; i += 2) {
			data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
					+ Character.digit(s.charAt(i+1), 16));
		}
		return data;
	}

	public String encrypt (String data, String key) {
		byte[] bKey = string2Bytes(key);
		SecretKeySpec skeySpec = new SecretKeySpec(bKey, "AES");

		Cipher cipher;
		try {
			cipher = Cipher.getInstance("AES");
			cipher.init(Cipher.ENCRYPT_MODE, skeySpec);

			byte[] encrpyted = cipher.doFinal(data.getBytes());

			return bytes2String(encrpyted);


		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BadPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;		
	}

	public String decrypt (String data, String key) {
		byte[] bKey = string2Bytes(key);
		SecretKeySpec skeySpec = new SecretKeySpec(bKey, "AES");

		Cipher cipher;
		try {
			cipher = Cipher.getInstance("AES");
			cipher.init(Cipher.DECRYPT_MODE, skeySpec);

			byte[] decrypted = cipher.doFinal(string2Bytes(data));

			return convertHexToString(bytes2String(decrypted));


		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BadPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;		
	}

	/*
	 * http://www.mkyong.com/java/how-to-convert-hex-to-ascii-in-java/
	 */
	public String convertHexToString(String hex){

		StringBuilder sb = new StringBuilder();
		StringBuilder temp = new StringBuilder();

		//49204c6f7665204a617661 split into two characters 49, 20, 4c...
		for( int i=0; i<hex.length()-1; i+=2 ){

			//grab the hex in pairs
			String output = hex.substring(i, (i + 2));
			//convert hex to decimal
			int decimal = Integer.parseInt(output, 16);
			//convert the decimal to character
			sb.append((char)decimal);

			temp.append(decimal);
		}
		return sb.toString();
	}


}
